<?php
/*
 * description：
 * author：wh
 * email：
 * createTime：{2024/5/15} {21:40}
 */

namespace wanghua\general_utility_tools_php\api;


use wanghua\general_utility_tools_php\gpt\chat\ChatGPT;
use wanghua\general_utility_tools_php\phpmailer\Exception;
use wanghua\general_utility_tools_php\tool\Tools;

/**
 * desc：接口文档构建器
 *
 * 使用步骤：
 * (new ApiDocument())->buildDoc();
 * Class ApiDocument
 */
class ApiDocument
{
    public $app_name = 'api';
    private $api_cache_arr = [];//缓存所有接口
    public $api_domain = 'http://127.0.0.1:8080/';//接口域名/ip
    /**
     * @var string 接口控制器命名空间
     */
    public $namespace = 'app\\api\\controller';
    /**
     * @var string 接口控制器基类，精确到类名，如：app\\common\\controller\\Api
     * 注意：如果同级目录存在多个基类，则设置直接基类，如果同级目录没有基类，则设置底层基类
     */
    public $extends_base_class = '';
    //控制器目录物理路径
    public $controllerDirectory = '';
    /**
     * @var string 接口文档保存路径
     */
    public $api_docs_save_dir = 'public/api_docs/';
    //设置过滤类
    private $filterClassArr = [];
    //设置过滤方法
    private $filterFunctionArr = [];

    public function __construct($api_domain='',$controllerDirectory='')
    {
        //默认，如果是tp6，那application就要改为app了，自行传参吧
        $this->controllerDirectory = Tools::get_root_path()."application/{$this->app_name}/controller";
        if($api_domain){
            $this->api_domain = $api_domain;
        }
        if($controllerDirectory){
            $this->controllerDirectory = $controllerDirectory;
        }
    }

    /**
     * desc：设置过滤类（类名）
     * 过滤场景：
     * 1、基类
     * 2、测试类
     * 3、定时任务类
     * 4、其它非必要类
     * author：wh
     * @param array $filterClassArr
     */
    function setFilterClass(array $filterClassArr=[]){
        $this->filterClassArr = $filterClassArr;
    }
    function setFilterFunction(array $filterArr=[]){
        $this->filterFunctionArr = $filterArr;
    }
    /**
     * desc：构建接口文档，支持同步到在线文档
     * author：wh
     */
    function buildDoc(){
        if(empty($this->extends_base_class)){
            throw new Exception('请设置接口控制器基类，精确到类名，如：app\\common\\controller\\Api');
        }
        $out_path = Tools::get_root_path().$this->api_docs_save_dir;
        if(!file_exists($out_path)){
            mkdir($out_path,0777,true);
        }
        $outputFile = $out_path.'api_list.md';
        $controllerClasses = [];

        // 搜索控制器目录下的所有PHP文件
        foreach (glob($this->controllerDirectory . '/*.php') as $filename) {
            // 获取文件中的类名
            $class = basename($filename, '.php');
            if(in_array($class,$this->filterClassArr)){
                continue;
            }
            // 构建完整的命名空间类名
            $fullClassName = $this->namespace . '\\' . $class;
            foreach (explode(',',$this->extends_base_class) as $base_class){
                // 检查类是否有效并且是think\Controller的子类
                if (class_exists($fullClassName) && is_subclass_of($fullClassName, $base_class)) {
                    $controllerClasses[] = $fullClassName;
                }
            }
        }

        // 创建Markdown文件
        $file = fopen($outputFile, 'w') or die('无法创建文件');

        $head_text = <<<EOF
# API 文档 
## 接口列表
###### （ctrl+f 搜索）（如果更改了路由，请根据路由规则定位）
##### 请求域名：{$this->api_domain}
##### 请求方式：POST（默认）

EOF;
        // 写入Markdown文件头部
        fwrite($file, $head_text);

        foreach ($controllerClasses as $controllerClass) {
            $reflector = new \ReflectionClass($controllerClass);
            // 遍历控制器中的公共方法
            $methods = $reflector->getMethods(\ReflectionMethod::IS_PUBLIC);
            foreach ($methods as $method) {
                //过滤方法
                if(in_array($method->name, $this->filterFunctionArr)){
                    continue;
                }
                $exp_class = explode('\\',$controllerClass);
                //过滤类
                if(in_array($exp_class[count($exp_class)-1],$this->filterClassArr)){
                    continue;
                }
                $comments = $method->getDocComment();
                if ($comments) {
                    $this->processMethodComment($comments, $controllerClass, $method->name, $outputFile);
                }
            }
        }
        fclose($file);

        //缓存所有接口
        cache('api_doc_cache_arr',$this->api_cache_arr);
    }


    /**
     * desc：解析方法注释并写入Markdown文件
     * author：wh
     * @param $comments
     * @param $className
     * @param $methodName
     * @param $savepath
     */
    private function processMethodComment($comments, $className, $methodName, $savepath) {
        if($methodName == '__construct'){
            return '';
        }
        $api_url = "/{$this->app_name}/{$className}/{$methodName}";
        $js_api_func_name = "api_{$className}_{$methodName}";
        $str = <<<EOF

***
```
    
EOF;

        $comments_str = mb_substr($comments,0,-2);
        $class_arr = explode('\\',$className);
        $className = $class_arr[count($class_arr)-1];
        $className = $this->camelCaseToUnderscore($className);


        $api_name = "{$this->app_name}/{$className}/{$methodName}";
        $doc_txt = <<<EOF
     * $api_name
     */
```
EOF;
        $doc_txt= $str.$comments_str.$doc_txt."    \r\n";
        $this->api_cache_arr[$className][] = ['api_name'=>$api_name,'doc_txt'=>$doc_txt,'class_name'=>$className];
        file_put_contents($savepath,$doc_txt, FILE_APPEND);
    }

    /**
     * desc：驼峰转下划线
     * author：wh
     * @param $string
     * @return string
     */
    private function camelCaseToUnderscore($string) {
        $str = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $string));
        return strpos($str,'_')===0?substr($str,1):$str;
    }

    //构建接口文档html
    function buildApiDocHtml(){
        //给定一组浅色背景色码
        $color_code_arr = [
            '#f0f8ff', '#f0ffff', '#f5f5dc', '#ffe4c4', '#f5f5f5', '#f5fffa', '#fff5ee', '#f8f8ff', '#fffaf0', '#fffff0', '#fafad2', '#f0fff0', '#fff0f5', '#ffe4e1', '#f0ffff', '#f0f8ff', '#f8f8ff', '#faebd7', '#fff0f5', '#ffe4e1', '#ffe4b5', '#ffdead', '#dcdcdc', '#dda0dd', '#fffaf0', '#eee8aa', '#fffafa', '#f0fff0', '#f0fff0', '#f0f8ff', '#f0ffff', '#f5f5dc', '#ffe4c4', '#f5f5f5', '#f5fffa', '#fff5ee', '#f8f8ff', '#fffaf0', '#f0fff0', '#fff0f5', '#ffe4e1', '#f0ffff', '#f0f8ff', '#f8f8ff', '#faebd7', '#fff0f5', '#ffe4e1', '#ffe4b5', '#ffdead', '#dcdcdc', '#dda0dd', '#fffaf0', '#eee8aa', '#fffafa', '#f0fff0', '#f0fff0', '#f0f8ff', '#f0ffff', '#f5f5dc', '#ffe4c4', '#f5f5f5', '#f5fffa', '#fff5ee', '#f8f8ff', '#fffaf0',
        ];

        $api_doc_cache_arr = cache('api_doc_cache_arr');

        $htm_str = <<<EOF
<style>
.txt-lf{text-align: left}
</style>
<div style="width: 50%;margin: 0 auto;color: red;text-align: center;">
    <div class="txt-lf">文档说明：</div>
    <div class="txt-lf">1、如果没有明确说明，提交请求均使用post</div>
    <div class="txt-lf">2、此接口文档不包含websocket接口</div>
    <div class="txt-lf">3、接口参数之间使用“/”符号隔开</div>
    <div class="txt-lf">4、此文档接口测试功能只针对普通post、get接口，不能测试文件上传或文件流</div>
    <div class="txt-lf">5、功能模块按照颜色分组</div>
    <div class="txt-lf" style="color: black;">清理缓存：
        <a href='JavaScript:;' onclick="CacheObj.clearCache()">
        点击清理
        </a><span style="color: gray;font-size: 12px;">（由于接口数据可能会被缓存，发现数据没变化或需要时，可清理缓存）</span>   
    </div>
    <div class="txt-lf">
        <a href="/api_docs/api_list.html" style="margin: 20px">api应用文档</a>
        <a href="/api_docs/{$this->app_name}_list.html" style="margin: 20px">{$this->app_name}应用文档</a>
    </div>

    
</div>

EOF;

        $script_str = "";
        foreach ($api_doc_cache_arr as $class_name_key=>$func_arr){
            //随机取一个颜色
            $color_code = $color_code_arr[array_rand($color_code_arr,1)];
            //是否显示上边距
            $is_show_margin_top = 'margin-top: 50px;';
            foreach ($func_arr as $k => $item){
                $api_name = $item['api_name'];
                $doc_txt = $item['doc_txt'];

                if($k > 0){

                    $is_show_margin_top = '';
                }


                $function_name = str_replace('/','_',$api_name);
                $htm_str .= <<<EOF
<div id="{$function_name}" style="background-color: {$color_code};{$is_show_margin_top}">
    <div class="markdown_content">{$doc_txt}</div>
    <div>
        按需填写其它接口参数：
        <textarea name="" id="{$function_name}_textarea" cols="100" rows="3">/{$api_name}</textarea>
        <a href='JavaScript:;' onclick="DocObject.{$function_name}()">测试</a>
    </div>
    <div class="{$function_name}_response_result"></div>
    
</div>
EOF;
                $script_str.=<<<EOF
        {$function_name}(){
            let url = $('#{$function_name}_textarea').val();
            $.post(url,{},function(res) {
                $('.{$function_name}_response_result').html(JSON.stringify(res, null, "\\t"));
                $('.{$function_name}_response_result').attr('style','color:green');
            },'json');
        },
EOF;

            }
        }
        $html = <<<EOF
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>接口文档</title>
</head>
<body>
    <div>
        {$htm_str}
    </div>
</body>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="/webautocodestatic/marked.min.js"></script>
<script src="/webautocodestatic/plugs/layui-v2.9.2/layui.js"></script>
<script>
    $(function() {

        //加载markdown
        DocObject.markdown_content();
    });
    
    let CacheObj = {
        clearCache: function () {
            let url = '/index/test/clearCache';
            $.post(url,{},function(res) {
                layer.msg('清理成功')
            },'json');
        }
    };
    
    let DocObject = {
         markdown_content(){
            $('.markdown_content').each(function(k,ele) {
                $(ele).html(marked.parse($(ele).html()));
            });
        },
        $script_str
    }
   
</script>
</html>
EOF;
        return $html;
    }


    /**
     * desc：生成ai接口文档
     * author：wh
     * @param $ai_config
     * @param $file_name
     * @param $file_path
     */
    function buildAiApiDocumentHtml($ai_config,$file_name,$file_path){
        $prompt = $this->getAiPrompt();
        //一般来说是通用的
        $ai_config['response_format'] = ['type' => 'text'];
        $file_code = file_get_contents($file_path);
        $str = <<<EOF
接口代码：
```php
{$file_code}
```


{$prompt}

EOF;
        Tools::log_to_write_txt(['生成文档提示词：'=>$str]);
        $this->outputStreamtream($ai_config,$str,$file_path);
    }
    private function getAiPrompt(){
        $prompt = <<<EOF

生成Layui风格的HTML接口文档，要求：

接口结构规范(div+css)：
生成接口标题，如查询订单
生成接口请求方式：默认POST
生成接口url: 显示url，/api/getuser/{id}，如果有参数则按照则按照“/”拼接
生成a标签测试按钮，点击跳转新页面
生成接口返回结构：
```json
{
    "code": 200,
    "msg": "ok",
    "data": {
        
    }
}
```

表格规范(table)：
生成参数表格包含：字段名｜注释｜是否必须｜类型
使用layui-table样式
每个接口之间采用卡片风格独立显示

美化：
从边距，边框，阴影，圆角等方面美化界面


正确文档结构：
```html

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link href="/static/plugins/layui/dist/css/layui.css" rel="stylesheet">
<script src="/static/js/jquery-3.5.1.js"></script>
<script src="/static/plugins/layui/dist/layui.js"></script>
</head>
<body>
<!-- 接口内容 -->
</body>
</html>
```


测试按钮规范
```html
<a class="layui-btn layui-btn-xs" 
   href="[URL]/[param1]/123/[param2]/123" 
   target="_blank">测试</a>
```


强制要求：
仅输出完整HTML文档, 禁用Markdown标记，仅输出纯HTML
路径参数自动替换为示例值
跳过所有非代码内容解释
仅对public方法生成,private和protected方法不生成
已经注释掉的接口不要生成


[节省token设计]
使用符号分隔替代完整句式
用代码片段代替文字描述
合并重复格式要求
删除所有敬语和过渡句

EOF;
        return $prompt;
    }

    function getApiFiles(){
        $files = [];
        // 搜索控制器目录下的所有PHP文件
        foreach (glob($this->controllerDirectory . '/*.php') as $filename) {
            // 获取文件中的类名
            $class = basename($filename, '.php');
            if(in_array($class,$this->filterClassArr)){
                continue;
            }
            // 构建完整的命名空间类名
            $fullClassName = $this->namespace . '\\' . $class;
            foreach (explode(',',$this->extends_base_class) as $base_class){
                // 检查类是否有效并且是think\Controller的子类
                if (class_exists($fullClassName) && is_subclass_of($fullClassName, $base_class)) {
                    $files[] = [
                        'file_name'=>$class,
                        'file_path'=>$filename
                    ];;
                }
            }
        }
        return $files;
    }

    //接口名翻译为中文
    function getApiNameTranslate($apis){
        $apis_json = json_encode($apis);
        if(cache('ai_translate_'.md5($apis_json))){
            return cache('ai_translate_'.md5($apis_json));
        }
        //每分钟只能请求6次
        $ai_config = config('step_fun_ai_config');
        $url = $ai_config['chat']['base_url'];
        $apiKey = $ai_config['chat']['APIKey'];
        $model = $ai_config['chat']['model'];

        $answer_json_arr = [];

        $chat_obj = new ChatGPT();
        $chat_obj->url = $url;
        $chat_obj->apiKey = $apiKey;
        $chat_obj->model = $model;

        $question = <<<EOF
你现在是一名优秀的程序员，英文很好擅长代码领域的翻译。
提供翻译的参数：{$apis_json}
翻译为中文，只返回翻译结果。
将以下英文数组元素直译为中文，严格按此JSON格式返回：
[
    {
      "original": "apple",
      "translated": "苹果"
    },
    {
      "original": "hello",
      "translated": "你好"
    }
]

要求：
1. 保持数组元素顺序不变
2. 仅翻译value，不修改key
3. 必须为合法JSON格式
4. 不要添加额外说明
EOF;

        $chat_obj->returnAnswer($question,["stream" => false],$answer_json_arr);

        $json_arr = json_decode($answer_json_arr[0],true);
        $trans_res_arr = $json_arr['choices'][0]['message']['content'];
        //缓存翻译结果
        cache('ai_translate_'.md5($apis_json),$trans_res_arr);
        return $trans_res_arr;
    }

    /**
     * author：wh
     * @param string $question
     * @param array $config
     * @param $answer_json_arr
     * @return string
     */
    function outputStreamtream($config, $question, $file_path)
    {
        $this->curlPostChat($question, $config, function ($ch, $data) use ($file_path) {
            echo $data;
            ob_flush();
            flush();
            return strlen($data);
        });
    }

    /**
     * author：wh
     * @param string $question
     * @param array $config
     * @param $callback
     */
    private function curlPostChat($question, $config = [], $callback)
    {
        $url = $config['base_url'];
        $apiKey = $config['APIKey'];
        //$model = $config['model'];
        $headers = ["Authorization: Bearer $apiKey", 'Accept: application/json', 'Content-Type: application/json',];
        $post_msg_body = ['stream' => true];
        if ($config) {
            foreach ($config as $key => $val) {
                $post_msg_body[$key] = $val;
            }
        }
        $messages = [];
        if ($question) {
            $messages[] = ["role" => "user", "content" => $question];
        }
        $post_msg_body['messages'] = $messages;
        //$this->post_msg_body = $post_msg_body;
        $postData = json_encode($post_msg_body);
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
        curl_setopt($ch, CURLOPT_WRITEFUNCTION, $callback);
        curl_exec($ch);
    }
}